using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.PowerShell.CmdletManagement.PsSpecCmdletService;

namespace Microsoft.PowerShell.CmdletManagement.CompareCmdlet
{
    public class CmdletInfo : PsSpecCmdlet
    {
        protected List<IMismatch> _mismatches = new List<IMismatch>();
        protected List<NamedParameterSet> _namedParameterSets = new List<NamedParameterSet>();
        protected NonameParameterSet _nonameParameterSet = null;

        public CmdletInfo(string cmdletName)
        {
            string[] verbNoun = cmdletName.Split('-');
            Verb = verbNoun[0];
            if (verbNoun.Length == 1)
                // A noun does not exist
                Noun = "";
            if (verbNoun.Length >= 2)
            {
                // Restore an original noun
                bool first = true;
                for (int i = 1; i < verbNoun.Length; i++)
                {
                    if (!first)
                        Noun += '-';
                    else
                        first = false;
                    Noun += verbNoun[i];
                }
            }
        }

        public CmdletInfo(string verb, string noun)
        {
            Verb = verb;
            Noun = noun;
        }

        public NonameParameterSet NonameParameterSet
        {
            get { return _nonameParameterSet; }
        }

        public List<NamedParameterSet> NamedParameterSets
        {
            get { return _namedParameterSets; }
        }

        public int ParameterSetCount
        {
            get
            {
                if (_nonameParameterSet != null)
                    return 1;
                return _namedParameterSets.Count;
            }
        }

        public string CompositeName()
        {
            return Verb + '-' + Noun;
        }

        ///<summary>Verify if a provided cmdlet has the same name as this cmdlet</summary>
        public bool HasName(CmdletInfo cmdlet)
        {
            return Noun.Equals(cmdlet.Noun, StringComparison.OrdinalIgnoreCase) &&
                   Verb.Equals(cmdlet.Verb, StringComparison.OrdinalIgnoreCase);
        }

        public void AddNonameParameterSet(NonameParameterSet nonameParameterSet)
        {
            _nonameParameterSet = nonameParameterSet;
            _nonameParameterSet.CmdletInfo = this;
        }

        public void AddNamedParameterSet(NamedParameterSet namedParameterSet)
        {
            _namedParameterSets.Add(namedParameterSet);
            namedParameterSet.CmdletInfo = this;
        }

        protected string ComposeParameterSetNames()
        {
            bool first = true;
            StringBuilder names = new StringBuilder();
            foreach (NamedParameterSet parameterSet in _namedParameterSets)
            {
                if (first)
                    first = false;
                else
                    names.Append(", ");
                names.Append(parameterSet.Name);
            }
            return names.ToString();
        }

        public void AddMismatches(CmdletInfo cmdletData)
        {
            _mismatches.AddRange(cmdletData._mismatches);
        }

        public void AddMismatch(IMismatch mismatch)
        {
            _mismatches.Add(mismatch);
        }
    }

    public class SpecifiedCmdletInfo : CmdletInfo
    {
        public SpecifiedCmdletInfo(CmdletInfo cmdlet) : base(cmdlet.Verb, cmdlet.Noun)
        {
        }

        public SpecifiedCmdletInfo(PsSpecCmdletService.PsSpecCmdlet aspenCmdlet)
            : base(aspenCmdlet.Verb, aspenCmdlet.Noun)
        {
            Snapin = aspenCmdlet.Snapin;
            ProjectName = aspenCmdlet.ProjectName;
            ShortDescription = aspenCmdlet.ShortDescription;
            LongDescription = aspenCmdlet.LongDescription;
            SupportsShouldProcess = aspenCmdlet.SupportsShouldProcess;
            Notes = aspenCmdlet.Notes;
        }

        public void CompareToImplementation(ImplementedCmdletInfo implementedCmdletInfo)
        {
            IMismatch mismatch;
            if (ParameterSetCount != implementedCmdletInfo.ParameterSetCount)
            {
                mismatch = new CmdletMismatch(this,
                                              String.Format(
                                                  "Specified [{0}] parameter sets but implemented [{1}] parameter sets.",
                                                  ParameterSetCount, implementedCmdletInfo.ParameterSetCount));
                _mismatches.Add(mismatch);
            }
            if (_nonameParameterSet != null)
            {
                // This cmdlet has only no name parameter set
                if (implementedCmdletInfo.NonameParameterSet == null)
                {
                    // Specified a no name parameter set is not implemented
                    mismatch = new CmdletMismatch(this,
                                                  String.Format(
                                                      "Specified a no name parameter set but implemented named parameter sets [{0}].",
                                                      ComposeParameterSetNames()));
                    _mismatches.Add(mismatch);
                    return;
                }
                _nonameParameterSet.CompareToImplementation(implementedCmdletInfo.NonameParameterSet);
            }
            else
            {
                // This cmdlet has several named parameter sets
                if (implementedCmdletInfo.NonameParameterSet != null)
                {
                    //
                    mismatch = new CmdletMismatch(this,
                                                  String.Format(
                                                      "Specified named parameter sets [{0}] but implemented a no name parameter set.",
                                                      ComposeParameterSetNames()));
                    _mismatches.Add(mismatch);
                    return;
                }
                // Verify named parameter sets
                foreach (NamedParameterSet specsParameterSet in _namedParameterSets)
                {
                    bool parameterSetFound = false;
                    foreach (NamedParameterSet implementedParameterSet in implementedCmdletInfo.NamedParameterSets)
                    {
                        if (implementedParameterSet.HasName(specsParameterSet))
                        {
                            specsParameterSet.CompareToImplementation(implementedParameterSet);
                            parameterSetFound = true;
                            break;
                        }
                    }
                    if (!parameterSetFound)
                    {
                        mismatch = new CmdletMismatch(this,
                                                      String.Format(
                                                          "The specified parameter set [{0}] is not implemented.",
                                                          specsParameterSet.Name));
                        _mismatches.Add(mismatch);
                    }
                }
            }
        }

        public static LinkedList<SpecifiedCmdletInfo> RetrieveCmdletInfoFromDatabase(string projectName,
                                                                                     CmdletService service)
        {
            return RetrieveCmdletInfoFromDatabase(projectName, service, "*");
        }

        public static LinkedList<SpecifiedCmdletInfo> RetrieveCmdletInfoFromDatabase(string projectName,
                                                                                     CmdletService service,
                                                                                     string cmdletName)
        {
            LinkedList<SpecifiedCmdletInfo> cmdlets = new LinkedList<SpecifiedCmdletInfo>();

            // Get all cmdlets for this request as a list
            PsSpecCmdletService.PsSpecCmdlet[] aspenCmdlets = service.GetCmdlet(
                PsSpecCmdletUtils.ConvertWildcardToSqlWildcard(cmdletName),
                PsSpecCmdletUtils.ConvertWildcardToSqlWildcard(projectName));
            for (int i = 0; i < aspenCmdlets.Length; i++)
            {
                cmdlets.AddLast(new SpecifiedCmdletInfo(aspenCmdlets[i]));
            }

            PsSpecCmdletService.PsSpecCmdletParameter[] parametersFromDB;
            List<PsSpecCmdletParameterData> parameters;
            // Get all parameters info for every cmdlet on the list
            foreach (SpecifiedCmdletInfo cmdlet in cmdlets)
            {
                NonameParameterSet nonameParameterSet = new NonameParameterSet();
                List<NamedParameterSet> namedParameterSets = new List<NamedParameterSet>();

                parametersFromDB = service.GetPsSpecCmdletParameter("%", "%", "%", cmdlet.CompositeName(),
                                                                    cmdlet.ProjectName);
                parameters = new List<PsSpecCmdletParameterData>();

                PsSpecCmdletService.PsSpecCmdletParameterSetEntry[] entries =
                    service.GetPsSpecCmdletParameterSetEntry(cmdlet.ProjectName, cmdlet.CompositeName(), "%", "%");
                foreach (PsSpecCmdletService.PsSpecCmdletParameterSetEntry entry in entries)
                {
                    // There must be the same number or more of entries than there are parameters
                    // For every entry create a corresponding parameter
                    foreach (PsSpecCmdletService.PsSpecCmdletParameter aspenParameter in parametersFromDB)
                    {
                        if (aspenParameter.Name.Equals(entry.ParameterName, StringComparison.InvariantCultureIgnoreCase))
                        {
                            // The matching parameter is found
                            PsSpecCmdletParameterData parameter = new PsSpecCmdletParameterData(aspenParameter, entry);
                            parameters.Add(parameter);
                            bool found = false;
                            if (entry.ParameterSetName != null && entry.ParameterSetName != "")
                            {
                                // If a parameter set does not exist, add it
                                foreach (NamedParameterSet parameterSet in namedParameterSets)
                                {
                                    if (parameterSet.HasName(entry.ParameterSetName))
                                    {
                                        // A parameter set already exists
                                        found = true;
                                        parameterSet.AddParameter(parameter);
                                    }
                                }
                                if (!found)
                                {
                                    // A parameter set does not exist. Create a new one
                                    NamedParameterSet nps = new NamedParameterSet(entry.ParameterSetName);
                                    namedParameterSets.Add(nps);
                                    nps.AddParameter(parameter);
                                }
                            }
                            else
                            {
                                // Parameter without a parameter set
                                nonameParameterSet.AddParameter(parameter);
                            }
                        }
                    }
                }
                if (namedParameterSets.Count > 0)
                {
                    // There are several named parameter sets
                    // Add common parameters to every named parameter set
                    foreach (NamedParameterSet parameterSet in namedParameterSets)
                    {
                        foreach (PsSpecCmdletParameterData parameter in nonameParameterSet.Parameters)
                            parameterSet.AddParameter(parameter);
                        cmdlet.AddNamedParameterSet(parameterSet);
                    }
                }
                else
                {
                    // Only a noname parameter set exists, so add it with all parameters
                    cmdlet.AddNonameParameterSet(nonameParameterSet);
                }
            }

            return cmdlets;
        }

        public void AddMismatchesToList(List<IMismatch> store)
        {
            store.AddRange(_mismatches);
            if (_nonameParameterSet != null)
            {
                // A single no name parameter set
                _nonameParameterSet.AddMismatchesToList(store);
                return;
            }
            // Multiple named parameter sets
            foreach (NamedParameterSet parameterSet in _namedParameterSets)
                parameterSet.AddMismatchesToList(store);
        }
    }

    public class ImplementedCmdletInfo : CmdletInfo
    {
        private bool _specified = false;
        protected Type _type;

        public ImplementedCmdletInfo(CmdletAttribute cmdletDeclaration, Type type)
            : base(cmdletDeclaration.VerbName, cmdletDeclaration.NounName)
        {
            _type = type;
        }

        public bool Specified
        {
            get { return _specified; }
            set { _specified = value; }
        }

        public void RestoreImplementedData()
        {
            // Initially there are no parameter sets
            NonameParameterSet currentParameterSet = null;
            PsSpecCmdletParameterData currentParameter = null;
            //Walk through each public member of cmdlet implementation

            foreach (PropertyInfo classMember in _type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                //Get attributes of each member and check if they are of interest
                foreach (
                    ParameterAttribute parameter in classMember.GetCustomAttributes(typeof (ParameterAttribute), true))
                {
                    // Every parameter has attribute info
                    string parameterSetName = parameter.ParameterSetName;
                    if (parameterSetName == "__AllParameterSets")
                    {
                        // It is a noname parameter set
                        if (_nonameParameterSet == null)
                            AddNonameParameterSet(new NonameParameterSet());
                        currentParameterSet = _nonameParameterSet;
                    }
                    else
                    {
                        currentParameterSet = RestoreParameterSet(parameterSetName);
                    }
                    currentParameter = new PsSpecCmdletParameterData();
                    currentParameter.Name = classMember.Name;
                    currentParameter.Type = classMember.PropertyType.Name + ',' + classMember.PropertyType.FullName;
                    // Parameter attribute info
                    currentParameter.Position = parameter.Position;
                    currentParameter.Mandatory = parameter.Mandatory;
                    currentParameter.ValueFromPipeline = parameter.ValueFromPipeline;
                    currentParameter.ValueFromPipelineByPropertyName = parameter.ValueFromPipelineByPropertyName;
                    currentParameter.MemberInfo = classMember;

                    //
                    // In some cmdlets implemented by inheriting from other cmdlets, reflection will return two propertyInfos with the same name.
                    // One cmdlet where this happens is enter-pssession.  What happens is that the base class implements a parameter
                    // with a property and the derived class declares another property with the same name, using the "new" keyword.
                    // Thus, you get two properties with the same name.  Furthermore, in some cmdlets, such as enter-pssession, both
                    // properties have parameter attributes.  
                    //
                    // For these cases we want to find the property of the most derived class and use that one.  That's what we will do
                    // here.  We will check if the parameter set has a parameter with the same name.  If it does, then we will compare
                    // that one with the one we are processing now and we'll keep the one of the most derived type.
                    //
                    // If there, isn't a parameter with a conflicting name, then we can just add the new one to the set.
                    //
                    bool keepExistingParameter = false;
                    PsSpecCmdletParameterData existingParameter;
                    if (currentParameterSet.TryGetParameter(currentParameter.Name, out existingParameter))
                    {
                        if (
                            currentParameter.MemberInfo.DeclaringType.IsSubclassOf(
                                existingParameter.MemberInfo.DeclaringType))
                        {
                            currentParameterSet.RemoveParameter(existingParameter);
                        }
                        else
                        {
                            keepExistingParameter = true;
                        }
                    }

                    if (! keepExistingParameter)
                    {
                        currentParameterSet.AddParameter(currentParameter);
                    }
                }
            }
            if (_namedParameterSets.Count > 0)
            {
                if (_nonameParameterSet != null)
                {
                    // Add common parameters to every named parameter set
                    foreach (NamedParameterSet namedParameterSet in _namedParameterSets)
                    {
                        foreach (PsSpecCmdletParameterData parameter in _nonameParameterSet.Parameters)
                        {
                            namedParameterSet.AddParameter(parameter);
                        }
                    }
                }
                _nonameParameterSet = null;
            }
            else if (_nonameParameterSet == null)
                // This cmdlet does not have any parameters, so add an empty no name parameter set
                AddNonameParameterSet(new NonameParameterSet());
        }

        private NonameParameterSet RestoreParameterSet(string parameterSetName)
        {
            foreach (NamedParameterSet parameterSet in _namedParameterSets)
            {
                if (parameterSet.Name == parameterSetName)
                    // A parameter set already exists
                    return parameterSet;
            }
            // A parameter set does not exist. Create a new one
            NamedParameterSet newParameterSet = new NamedParameterSet(parameterSetName);
            AddNamedParameterSet(newParameterSet);
            return newParameterSet;
        }

        public static LinkedList<ImplementedCmdletInfo> RetrieveImplementedCmdlets(List<Assembly> assemblies)
        {
            LinkedList<ImplementedCmdletInfo> cmdlets = new LinkedList<ImplementedCmdletInfo>();
            ImplementedCmdletInfo cmdlet;
            foreach (Assembly assmbly in assemblies)
            {
                foreach (Type assemblyType in assmbly.GetTypes())
                {
                    foreach (
                        CmdletAttribute cmdletDeclaration in
                            assemblyType.GetCustomAttributes(typeof (CmdletAttribute), true))
                    {
                        if (cmdletDeclaration != null)
                        {
                            // Create a new instance for every implemented cmdlet
                            cmdlet = new ImplementedCmdletInfo(cmdletDeclaration, assemblyType);
                            cmdlets.AddLast(cmdlet);
                            cmdlet.RestoreImplementedData();
                        }
                    }
                }
            }
            return cmdlets;
        }

        public static LinkedList<ImplementedCmdletInfo> RetrieveImplementedCmdlets(List<Assembly> assemblies, Regex name)
        {
            LinkedList<ImplementedCmdletInfo> cmdlets = new LinkedList<ImplementedCmdletInfo>();
            ImplementedCmdletInfo cmdlet;
            foreach (Assembly assmbly in assemblies)
            {
                foreach (Type assemblyType in assmbly.GetTypes())
                {
                    foreach (
                        CmdletAttribute cmdletDeclaration in
                            assemblyType.GetCustomAttributes(typeof (CmdletAttribute), true))
                    {
                        if (cmdletDeclaration != null &&
                            (name.Match(cmdletDeclaration.VerbName + "-" + cmdletDeclaration.NounName)).Success)
                        {
                            // Create a new instance for a matched implemented cmdlet
                            cmdlet = new ImplementedCmdletInfo(cmdletDeclaration, assemblyType);
                            cmdlets.AddLast(cmdlet);
                            cmdlet.RestoreImplementedData();
                        }
                    }
                }
            }
            return cmdlets;
        }

        public static LinkedList<ImplementedCmdletInfo> RetrieveImplementedCmdlets(List<Assembly> assemblies,
                                                                                   string verb, string noun)
        {
            LinkedList<ImplementedCmdletInfo> cmdlets = new LinkedList<ImplementedCmdletInfo>();
            ImplementedCmdletInfo cmdlet;
            foreach (Assembly assmbly in assemblies)
            {
                foreach (Type assemblyType in assmbly.GetTypes())
                {
                    foreach (
                        CmdletAttribute cmdletDeclaration in
                            assemblyType.GetCustomAttributes(typeof (CmdletAttribute), true))
                    {
                        if (cmdletDeclaration != null &&
                            cmdletDeclaration.VerbName.Equals(verb, StringComparison.CurrentCultureIgnoreCase) &&
                            cmdletDeclaration.NounName.Equals(noun, StringComparison.CurrentCultureIgnoreCase))
                        {
                            // Create a new instance for a matched implemented cmdlet
                            cmdlet = new ImplementedCmdletInfo(cmdletDeclaration, assemblyType);
                            cmdlets.AddLast(cmdlet);
                            cmdlet.RestoreImplementedData();
                        }
                    }
                }
            }
            return cmdlets;
        }
    }
}